namespace SS.Common.Lock
{
    #region Used namespaces

    using System;
    using System.Diagnostics;
    using System.Threading;

    #endregion

    /// <summary>
    /// Lock class using CPU spin method
    /// </summary>
    public class SpinLock
    {
        /// <summary>
        /// The lock free to own
        /// </summary>
        private const int ISFREE = 0;

        /// <summary>
        /// The lock has been owned
        /// </summary>
        private const int ISOWNED = 1;

        /// <summary>
        /// Current lock state (free, owned)
        /// </summary>
        private int lockState;

        /// <summary>
        /// Counter part spin wait object
        /// </summary>
        private readonly SpinWait spinWait;

        /// <summary>
        /// The index of the thread that currently hold the lock
        /// </summary>
        private int threadWhoTookLock;

        /// <summary>
        /// Should the lock track who is the thread owner
        /// </summary>
        private readonly bool isThreadOwnerTrackingEnabled;

        /// <summary>
        /// Is owned thread tracking enabled
        /// </summary>
        public bool IsThreadOwnerTrackingEnabled
        {
            get
            {
                return this.isThreadOwnerTrackingEnabled;
            }
        }

        /// <summary>
        /// Is owned thread the same with current thread
        /// </summary>
        public bool IsHeldByCurrentThread
        {
            get
            {
                if (this.isThreadOwnerTrackingEnabled)
                {
                    return this.lockState == ISOWNED &&
                           Thread.CurrentThread.ManagedThreadId == this.threadWhoTookLock;
                }

                return this.lockState == ISOWNED;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SpinLock"/> class. 
        /// </summary>
        /// <param name="trackId">Enable or disable tracking owned thread id
        /// </param>
        public SpinLock(bool trackId)
        {
            this.isThreadOwnerTrackingEnabled = trackId;
            this.threadWhoTookLock = 0;
            this.spinWait = new SpinWait();
        }

        /// <summary>
        /// Track owned lock
        /// </summary>
        /// <exception cref="LockRecursionException">When the lock has been aquired by current thread</exception>
        private void CheckAndSetThreadId()
        {
            if (this.threadWhoTookLock == Thread.CurrentThread.ManagedThreadId)
            {
                throw new LockRecursionException(
                    "The current thread has already acquired this lock.");
            }

            this.threadWhoTookLock = Thread.CurrentThread.ManagedThreadId;
        }

        /// <summary>
        /// Aquire the lock in a reliable way (handle exception)
        /// </summary>
        /// <param name="lockTaken">Ok to aquire the lock or not</param>
        public void ReliableEnter(ref bool lockTaken)
        {
            try
            {
                this.Enter();
                lockTaken = this.lockState == ISOWNED &&
                            Thread.CurrentThread.ManagedThreadId == this.threadWhoTookLock;
            }
            catch
            {
                lockTaken = false;
            }
        }

        /// <summary>
        /// Aquire the lock
        /// </summary>
        public void Enter()
        {
            var result = Interlocked.Exchange(ref this.lockState, ISOWNED);

            while (result == ISOWNED)
            {
                // If resource available, set it to in-use and return
                result = Interlocked.Exchange(ref this.lockState, ISOWNED);
                if (result == ISFREE)
                {
                    break;
                }

                // Efficiently spin, until the resource looks like it might 
                // be free. NOTE: Just reading here (as compared to repeatedly 
                // calling Exchange) improves performance because writing 
                // forces all CPUs to update this value
                while (Thread.VolatileRead(ref this.lockState) == ISOWNED)
                {
                    this.spinWait.SpinOnce();
                }
            }

            this.CheckAndSetThreadId();
        }

        /// <summary>
        /// Try to acquire the lock
        /// </summary>
        /// <returns>true if can aquire</returns>
        public bool TryEnter()
        {
            // If resource available, set it to in-use and return
            if (Interlocked.Exchange(ref this.lockState, ISOWNED) == ISFREE)
            {
                this.CheckAndSetThreadId();
                return true;
            }

            return false;
        }

        /// <summary>
        /// Try to aquire the lock in a specified time ammount
        /// </summary>
        /// <param name="timeout">The specified time ammount</param>
        /// <returns>true if can acquire</returns>
        public bool TryEnter(TimeSpan timeout)
        {
            return this.TryEnter((int)timeout.TotalMilliseconds);
        }

        /// <summary>
        /// Try to aquire the lock in a specified time ammount
        /// </summary>
        /// <param name="milliSeconds">The specified time ammount (ms)</param>
        /// <returns>true if can acquire</returns>
        public bool TryEnter(int milliSeconds)
        {
            // Thread.BeginCriticalRegion();
            var sw = Stopwatch.StartNew();
            var result = false;

            while (sw.ElapsedMilliseconds < milliSeconds)
            {
                if (Interlocked.Exchange(ref this.lockState, ISOWNED) != ISFREE)
                {
                    continue;
                }

                this.threadWhoTookLock = Thread.CurrentThread.ManagedThreadId;
                result = true;
            }

            sw.Stop();
            return result;
        }

        /// <summary>
        /// Try to enter in a reliable manner (handle all exception)
        /// </summary>
        /// <param name="lockTaken">Return lock has been taken by caller or not</param>
        public void TryReliableEnter(ref bool lockTaken)
        {
            try
            {
                lockTaken = this.TryEnter();
            }
            catch
            {
                lockTaken = false;
            }
        }

        /// <summary>
        /// Try to enter in a reliable manner (handle all exception) in specified time ammount
        /// </summary>
        /// <param name="timeout">The specified time ammount</param>
        /// <param name="lockTaken">Return lock has been taken by caller or not</param>
        public void TryReliableEnter(TimeSpan timeout, ref bool lockTaken)
        {
            TryReliableEnter((int)timeout.TotalMilliseconds, ref lockTaken);
        }

        /// <summary>
        /// Try to enter in a reliable manner (handle all exception) in specified time ammount
        /// </summary>
        /// <param name="milliSeconds">The specified time ammount</param>
        /// <param name="lockTaken">Return lock has been taken by caller or not</param>
        public void TryReliableEnter(int milliSeconds, ref bool lockTaken)
        {
            // Thread.BeginCriticalRegion();
            var sw = Stopwatch.StartNew();

            while (sw.ElapsedMilliseconds < milliSeconds)
            {
                this.TryReliableEnter(ref lockTaken);
            }

            sw.Stop();
        }

        /// <summary>
        /// Exit locking
        /// </summary>
        public void Exit()
        {
            this.threadWhoTookLock = int.MinValue;
            this.lockState = ISFREE;
        }

        /// <summary>
        /// Exit locking
        /// </summary>
        /// <param name="flushReleaseWrites">Flush all writes or not 
        /// (volatile write to lockstate)</param>
        public void Exit(bool flushReleaseWrites)
        {
            this.threadWhoTookLock = int.MinValue;

            // Mark the resource as available
            if (!flushReleaseWrites)
            {
                this.lockState = ISFREE;
            }
            else
            {
                Thread.VolatileWrite(ref this.lockState, ISFREE);
            }

            // Thread.EndCriticalRegion();
        }
    }
}
